Skip to content

feat: implement albums feature with create, edit, delete, and image management UI#610

Open
SiddharthJiyani wants to merge 8 commits intoAOSSIE-Org:mainfrom
SiddharthJiyani:feat/albums-ui-and-backend
Open

feat: implement albums feature with create, edit, delete, and image management UI#610
SiddharthJiyani wants to merge 8 commits intoAOSSIE-Org:mainfrom
SiddharthJiyani:feat/albums-ui-and-backend

Conversation

@SiddharthJiyani
Copy link
Contributor

@SiddharthJiyani SiddharthJiyani commented Nov 3, 2025

Description

This PR implements a complete Albums feature , including both backend API endpoints and a fully functional frontend UI.

Features Implemented

Backend

  • Improved Album CRUD operations (Create, Read, Update, Delete)
  • Album locking/password protection
  • Cover image management
  • Image management within albums (add, remove, reorder)
  • Proper error handling and validation

Frontend

  • Albums UI with album cards
  • Create album dialog with name, description, and lock option
  • Edit album dialog with ability to change lock status
  • Delete album with confirmation dialog
  • Password-protected access for locked albums
  • Add/remove images from albums
  • Set album cover image
  • Dark/Light theme support with appropriate placeholder images

Technical Details

Architecture

  • State Management: Redux with albumsSlice.ts for persistent state
  • API Integration: usePictoQuery and usePictoMutation hooks
  • UI Components: Standard ShadCN components (Dialog, Button, Input, Label, Switch, etc.)
  • User Feedback: infoDialogSlice.ts for messages and loaderSlice.ts for loading states
  • Type Safety: Full TypeScript coverage with proper interfaces

Key Changes

  1. Created Redux slice (albumsSlice.ts) with 11 actions for album state management
  2. Created memoized selectors (albumSelectors.ts) using createSelector
  3. Implemented API functions for all album operations (albums.ts)
  4. Built comprehensive album components:
    • AlbumCard.tsx - Album display with lock icon
    • CreateAlbumDialog.tsx - New album creation
    • EditAlbumDialog.tsx - Album editing with lock management
    • DeleteConfirmDialog.tsx - Delete confirmation
    • AlbumPasswordDialog.tsx - Password entry for locked albums
    • AddImagesToAlbumDialog.tsx - Image management
  5. Created album pages:
    • Album.tsx - Albums list view
    • AlbumDetail.tsx - Album detail view with images
  6. Added proper error handling with detailed backend error messages

Testing Checklist

  • Create album (locked and unlocked)
  • Edit album (change name, description, lock status)
  • Delete album with confirmation
  • Lock/unlock albums with password
  • Add/remove images from album
  • Set album cover image
  • Auto-refresh UI after operations
  • Error messages display properly
  • Dark/Light theme support
  • Responsive design on mobile and desktop

Related Issues

Tasks

  • Use standard ShadCN components to create the UI. (Refer to other components)
  • Store all the state information using Redux only. (Refer to similar files to know how to create a Redux slice and store the query results)
  • Use only usePictoQuery and usePictoMutation to handle API calling.
  • Utilize the standard infoDialogSlice.ts and loaderSlice.ts to display dialog messages and loaders, respectively.
pictopy-album-issue_.2.mp4

Summary by CodeRabbit

  • New Features

    • Albums now support locking (password protection), cover images, per-album photo counts, and a new API to set an album's cover.
  • UI

    • Full album UI: listing, detail/gallery, create, edit, delete, add-images, password prompt, album card, and empty-state components.
  • Behavior Changes

    • Album visibility uses "is_locked"; album list no longer accepts a show-hidden parameter and payloads include is_locked, cover_image_path, and image_count.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 3, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces album visibility is_hidden with is_locked, adds cover_image_path and image_count to album schema with a migration, adds cover-image update API and helpers, and implements a full frontend albums feature set (API clients, Redux slice, components, pages, routes, and types).

Changes

Cohort / File(s) Summary
Backend DB & Schema
backend/app/database/albums.py, backend/app/schemas/album.py
Rename is_hiddenis_locked; add cover_image_path and image_count; add migration db_migrate_add_cover_image_column(); tighten SELECT columns; add db_update_album_cover_image() and db_get_image_path(); update insert/update signatures.
Backend Routes & Tests
backend/app/routes/albums.py, backend/tests/test_albums.py
Remove show_hidden param; use is_locked semantics; include cover_image_path and image_count in responses; add PUT /albums/{album_id}/cover; update tests and fixtures (mock_db_locked_album).
Frontend API & Endpoints
frontend/src/api/apiEndpoints.ts, frontend/src/api/api-functions/albums.ts, frontend/src/api/api-functions/index.ts
Add albumsEndpoints and albums client functions (get/create/update/delete, image management, set cover); re-export in API index.
Frontend State
frontend/src/features/albumsSlice.ts, frontend/src/features/albumSelectors.ts, frontend/src/app/store.ts
New Redux slice for albums (albums, selectedAlbum, albumImages) with actions; selectors including memoized counts; integrate albumsReducer into root store.
Frontend Components — Albums UI
frontend/src/components/Albums/*.tsx, frontend/src/components/EmptyStates/EmptyAlbumsState.tsx
New components: AlbumCard, CreateAlbumDialog, EditAlbumDialog, DeleteConfirmDialog, AlbumPasswordDialog, AddImagesToAlbumDialog, empty-state UI. Components use cover_image_path, is_locked, and update flows.
Frontend Pages & Routing
frontend/src/pages/Album/Album.tsx, frontend/src/pages/Album/AlbumDetail.tsx, frontend/src/routes/AppRoutes.tsx, frontend/src/constants/routes.ts
Rename/implement Albums list page, add AlbumDetail page with gallery and cover-setting, update routes and add ALBUM_DETAIL route.
Frontend Types
frontend/src/types/Album.ts
Redefined Album shape (id, name, description, is_locked, cover_image_path, image_count, timestamps) and request/prop types (Create/Update/Add/Remove/Get images, dialog props); removed legacy list interfaces.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Albums UI
    participant API as Backend API
    participant DB as Database

    User->>UI: Open CreateAlbumDialog
    UI->>API: POST /albums {name,is_locked,password}
    API->>DB: db_insert_album(...)
    DB-->>API: OK
    API-->>UI: 201 Created
    UI->>API: GET /albums
    API->>DB: db_get_all_albums()
    DB-->>API: [albums with cover_image_path,image_count]
    API-->>UI: Album list
Loading
sequenceDiagram
    participant User
    participant Detail as AlbumDetail Page
    participant UIAdd as AddImagesToAlbumDialog
    participant API as Backend API
    participant DB as Database

    User->>Detail: View album
    Detail->>API: GET /albums/{id}
    API->>DB: db_get_album(id)
    DB-->>API: album (cover_image_path,image_count)
    API-->>Detail: album data
    User->>Detail: Set cover image (choose image)
    Detail->>API: PUT /albums/{id}/cover {image_id}
    API->>DB: db_get_image_path(image_id)
    API->>DB: db_update_album_cover_image(id, path)
    DB-->>API: OK
    API-->>Detail: 200 Updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Suggested reviewers

  • rahulharpal1603

Poem

🐇
I hopped through fields of code and locks,
Found cover frames and counted flocks.
Dialogs, cards, and an API lane,
I nibbled bugs, then hopped again—
Commit secured, I munch a carrot box.

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (88 files):

⚔️ .github/ISSUE_TEMPLATE/bug.yml (content)
⚔️ .github/ISSUE_TEMPLATE/feature.yml (content)
⚔️ .github/workflows/build-and-release.yml (content)
⚔️ .github/workflows/pr-check-build.yml (content)
⚔️ .gitignore (content)
⚔️ .pre-commit-config.yaml (content)
⚔️ CONTRIBUTING.md (content)
⚔️ backend/app/config/settings.py (content)
⚔️ backend/app/database/albums.py (content)
⚔️ backend/app/database/face_clusters.py (content)
⚔️ backend/app/database/faces.py (content)
⚔️ backend/app/database/images.py (content)
⚔️ backend/app/database/metadata.py (content)
⚔️ backend/app/logging/setup_logging.py (content)
⚔️ backend/app/models/FaceDetector.py (content)
⚔️ backend/app/routes/albums.py (content)
⚔️ backend/app/routes/images.py (content)
⚔️ backend/app/schemas/album.py (content)
⚔️ backend/app/utils/face_clusters.py (content)
⚔️ backend/main.py (content)
⚔️ backend/requirements.txt (content)
⚔️ backend/tests/test_albums.py (content)
⚔️ docs/Manual_Setup_Guide.md (content)
⚔️ docs/Script_Setup_Guide.md (content)
⚔️ docs/backend/backend_python/api.md (content)
⚔️ docs/backend/backend_python/openapi.json (content)
⚔️ docs/backend/backend_rust/api.md (content)
⚔️ docs/stylesheets/extra.css (content)
⚔️ frontend/package-lock.json (content)
⚔️ frontend/scripts/setup_env.sh (content)
⚔️ frontend/scripts/setup_win.ps1 (content)
⚔️ frontend/src-tauri/Cargo.lock (content)
⚔️ frontend/src-tauri/Cargo.toml (content)
⚔️ frontend/src-tauri/Entitlements.plist (content)
⚔️ frontend/src-tauri/capabilities/migrated.json (content)
⚔️ frontend/src-tauri/src/main.rs (content)
⚔️ frontend/src-tauri/src/services/mod.rs (content)
⚔️ frontend/src-tauri/tauri.conf.json (content)
⚔️ frontend/src/api/api-functions/index.ts (content)
⚔️ frontend/src/api/apiEndpoints.ts (content)
⚔️ frontend/src/app/store.ts (content)
⚔️ frontend/src/components/EmptyStates/EmptyGalleryState.tsx (content)
⚔️ frontend/src/components/Media/ImageCard.tsx (content)
⚔️ frontend/src/components/Media/MediaInfoPanel.tsx (content)
⚔️ frontend/src/components/Media/MediaThumbnails.tsx (content)
⚔️ frontend/src/components/Media/MediaView.tsx (content)
⚔️ frontend/src/components/Media/MediaViewControls.tsx (content)
⚔️ frontend/src/components/Media/NavigationButtons.tsx (content)
⚔️ frontend/src/components/Navigation/Sidebar/AppSidebar.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/AvatarSelectionStep.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/FolderSetupStep.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/OnboardingStep.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/ServerCheck.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/ThemeSelectionStep.tsx (content)
⚔️ frontend/src/config/Backend.ts (content)
⚔️ frontend/src/constants/routes.ts (content)
⚔️ frontend/src/features/onboardingSlice.ts (content)
⚔️ frontend/src/hooks/useFolderOperations.tsx (content)
⚔️ frontend/src/hooks/useSlideshow.ts (content)
⚔️ frontend/src/layout/layout.tsx (content)
⚔️ frontend/src/main.tsx (content)
⚔️ frontend/src/pages/Album/Album.tsx (content)
⚔️ frontend/src/pages/Home/Home.tsx (content)
⚔️ frontend/src/pages/SettingsPage/components/ApplicationControlsCard.tsx (content)
⚔️ frontend/src/routes/AppRoutes.tsx (content)
⚔️ frontend/src/types/Album.ts (content)
⚔️ frontend/src/types/Media.ts (content)
⚔️ landing-page/index.html (content)
⚔️ landing-page/src/Pages/Demo/marqu.tsx (content)
⚔️ landing-page/src/Pages/FaqPage/FAQ.tsx (content)
⚔️ landing-page/src/Pages/Footer/Footer.tsx (content)
⚔️ landing-page/src/Pages/HowItWorks/HowItWorks.tsx (content)
⚔️ landing-page/src/Pages/Landing page/Home1.tsx (content)
⚔️ landing-page/src/Pages/pictopy-landing.tsx (content)
⚔️ mkdocs.yml (content)
⚔️ scripts/setup.js (content)
⚔️ scripts/setup.ps1 (content)
⚔️ scripts/setup.sh (content)
⚔️ sync-microservice/README.md (content)
⚔️ sync-microservice/app/config/settings.py (content)
⚔️ sync-microservice/app/core/lifespan.py (content)
⚔️ sync-microservice/app/logging/setup_logging.py (content)
⚔️ sync-microservice/app/routes/folders.py (content)
⚔️ sync-microservice/app/routes/health.py (content)
⚔️ sync-microservice/app/routes/watcher.py (content)
⚔️ sync-microservice/app/utils/watcher_helpers.py (content)
⚔️ sync-microservice/main.py (content)
⚔️ sync-microservice/requirements.txt (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the primary feature being implemented - a complete albums feature with UI for create, edit, delete operations and image management.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SiddharthJiyani SiddharthJiyani force-pushed the feat/albums-ui-and-backend branch from ff90128 to afcecea Compare November 3, 2025 14:27
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/types/Album.ts (1)

59-92: Remove duplicate interface definitions.

PasswordPromptDialogProps is defined twice (lines 59-64 and 80-85), and AddImagesToAlbumDialogProps is also defined twice (lines 66-71 and 87-92). This creates confusion and potential maintenance issues if the definitions diverge.

Remove the duplicate definitions:

 export interface CreateAlbumDialogProps {
   isOpen: boolean;
   onClose: () => void;
   onSuccess?: () => void;
 }

 export interface PasswordPromptDialogProps {
   isOpen: boolean;
   onClose: () => void;
   onSubmit: (password: string) => void;
   albumName: string;
 }

+export interface AddImagesToAlbumDialogProps {
+  isOpen: boolean;
+  onClose: () => void;
+  albumId: string;
+  albumName: string;
+}
+
 export interface AlbumCardProps {
   album: Album;
   onClick: () => void;
   onEdit: () => void;
   onDelete: () => void;
 }
-
-export interface PasswordPromptDialogProps {
-  isOpen: boolean;
-  onClose: () => void;
-  onSubmit: (password: string) => void;
-  albumName: string;
-}
-
-export interface AddImagesToAlbumDialogProps {
-  isOpen: boolean;
-  onClose: () => void;
-  albumId: string;
-  albumName: string;
-}
🧹 Nitpick comments (4)
frontend/src/components/EmptyStates/EmptyAlbumsState.tsx (1)

3-28: Consider adding aria-hidden to decorative icons.

The component structure and implementation are excellent. The layout, styling, dark mode support, and user guidance are well-executed.

For enhanced accessibility, consider adding aria-hidden="true" to decorative icons on lines 7, 18, and 22, since they don't convey information beyond what the text already provides.

Example for line 7:

-        <BookImage className="h-16 w-16 text-gray-400" strokeWidth={1.5} />
+        <BookImage className="h-16 w-16 text-gray-400" strokeWidth={1.5} aria-hidden="true" />
frontend/src/pages/Album/AlbumDetail.tsx (2)

153-174: Backend data transformation handles schema variations defensively.

The transformation logic safely handles multiple possible backend response structures using optional chaining and defaults. However, the use of any types suggests the backend API contract could benefit from stronger typing.

Consider defining a TypeScript interface for the backend album response to eliminate the any casts and improve type safety:

interface BackendAlbum {
  album_id: string;
  album_name: string;
  description?: string;
  is_locked?: boolean;
  cover_image_path?: string;
  image_count?: number;
  created_at?: string;
  updated_at?: string;
}

176-213: Image loading works but has inefficient data fetching pattern.

The implementation fetches all images from the library, then filters by album image IDs. While functional, this becomes inefficient as the image library grows.

Consider updating the backend getAlbumImages endpoint to return full image objects rather than just IDs, eliminating the need to fetch and filter the entire image library:

// Instead of fetching all images
const { data: allImagesData } = usePictoQuery({
  queryKey: ['images'],
  queryFn: () => fetchAllImages(),
  enabled: !!albumId && !!album,
});

// Backend could return full image objects directly
const { data: imagesData } = usePictoQuery({
  queryKey: ['album-images', albumId],
  queryFn: () => getAlbumImages(albumId!, password ? { password } : undefined),
  enabled: !!albumId && !!album,
});
frontend/src/features/albumsSlice.ts (1)

56-70: Consider syncing image_count from server.

The local image_count updates (increment on add, decrement on remove) are correct, but could drift from the server if operations fail or are retried. Consider refreshing the selected album from the server after image operations complete to ensure consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe2f98d and afcecea.

⛔ Files ignored due to path filters (2)
  • frontend/public/placeholder-album-light.svg is excluded by !**/*.svg
  • frontend/public/placeholder-album.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • backend/app/database/albums.py (10 hunks)
  • backend/app/routes/albums.py (10 hunks)
  • backend/app/schemas/album.py (3 hunks)
  • frontend/src/api/api-functions/albums.ts (1 hunks)
  • frontend/src/api/api-functions/index.ts (1 hunks)
  • frontend/src/api/apiEndpoints.ts (1 hunks)
  • frontend/src/app/store.ts (2 hunks)
  • frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/AlbumCard.tsx (1 hunks)
  • frontend/src/components/Albums/AlbumPasswordDialog.tsx (1 hunks)
  • frontend/src/components/Albums/CreateAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/DeleteConfirmDialog.tsx (1 hunks)
  • frontend/src/components/Albums/EditAlbumDialog.tsx (1 hunks)
  • frontend/src/components/EmptyStates/EmptyAlbumsState.tsx (1 hunks)
  • frontend/src/constants/routes.ts (1 hunks)
  • frontend/src/features/albumSelectors.ts (1 hunks)
  • frontend/src/features/albumsSlice.ts (1 hunks)
  • frontend/src/pages/Album/Album.tsx (1 hunks)
  • frontend/src/pages/Album/AlbumDetail.tsx (1 hunks)
  • frontend/src/routes/AppRoutes.tsx (2 hunks)
  • frontend/src/types/Album.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (15)
frontend/src/components/Albums/AlbumCard.tsx (2)
frontend/src/types/Album.ts (1)
  • AlbumCardProps (73-78)
frontend/src/contexts/ThemeContext.tsx (1)
  • useTheme (66-73)
frontend/src/components/Albums/CreateAlbumDialog.tsx (5)
frontend/src/types/Album.ts (1)
  • CreateAlbumDialogProps (53-57)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (1)
  • createAlbum (37-45)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/components/Albums/AlbumPasswordDialog.tsx (1)
frontend/src/types/Album.ts (2)
  • PasswordPromptDialogProps (59-64)
  • PasswordPromptDialogProps (80-85)
backend/app/schemas/album.py (1)
frontend/src/types/Album.ts (1)
  • UpdateAlbumRequest (26-32)
frontend/src/features/albumsSlice.ts (2)
frontend/src/types/Album.ts (1)
  • Album (1-10)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/components/Albums/EditAlbumDialog.tsx (5)
frontend/src/types/Album.ts (1)
  • EditAlbumDialogProps (46-51)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (1)
  • updateAlbum (52-61)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/api/api-functions/albums.ts (4)
frontend/src/types/API.ts (1)
  • APIResponse (1-8)
frontend/src/api/axiosConfig.ts (1)
  • apiClient (5-12)
frontend/src/api/apiEndpoints.ts (1)
  • albumsEndpoints (33-46)
frontend/src/types/Album.ts (5)
  • CreateAlbumRequest (19-24)
  • UpdateAlbumRequest (26-32)
  • AddImagesToAlbumRequest (34-36)
  • GetAlbumImagesRequest (38-40)
  • RemoveImagesFromAlbumRequest (42-44)
backend/app/routes/albums.py (2)
backend/app/schemas/album.py (5)
  • SetCoverImageRequest (55-56)
  • GetAlbumsResponse (64-66)
  • Album (6-12)
  • SuccessResponse (84-86)
  • ErrorResponse (89-92)
backend/app/database/albums.py (5)
  • db_update_album_cover_image (221-232)
  • verify_album_password (299-311)
  • db_get_all_albums (107-119)
  • db_get_album_images (235-245)
  • db_get_album (136-147)
frontend/src/routes/AppRoutes.tsx (2)
frontend/src/constants/routes.ts (1)
  • ROUTES (1-12)
frontend/src/pages/Album/AlbumDetail.tsx (1)
  • AlbumDetail (43-437)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (7)
frontend/src/types/Album.ts (2)
  • AddImagesToAlbumDialogProps (66-71)
  • AddImagesToAlbumDialogProps (87-92)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/api/api-functions/albums.ts (1)
  • addImagesToAlbum (79-88)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/components/Albums/DeleteConfirmDialog.tsx (1)
frontend/src/components/ui/dialog.tsx (6)
  • Dialog (133-133)
  • DialogContent (135-135)
  • DialogHeader (138-138)
  • DialogTitle (141-141)
  • DialogDescription (136-136)
  • DialogFooter (137-137)
frontend/src/features/albumSelectors.ts (1)
frontend/src/app/store.ts (1)
  • RootState (24-24)
frontend/src/types/Album.ts (1)
backend/app/schemas/album.py (4)
  • CreateAlbumRequest (20-30)
  • UpdateAlbumRequest (33-44)
  • GetAlbumImagesRequest (47-48)
  • Album (6-12)
frontend/src/pages/Album/AlbumDetail.tsx (13)
frontend/src/features/albumSelectors.ts (2)
  • selectSelectedAlbum (7-8)
  • selectAlbumImages (10-10)
frontend/src/features/imageSelectors.ts (1)
  • selectIsImageViewOpen (13-16)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (4)
  • getAlbumById (26-31)
  • getAlbumImages (95-104)
  • removeMultipleImagesFromAlbum (126-135)
  • setAlbumCoverImage (142-151)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/types/Album.ts (1)
  • Album (1-10)
frontend/src/features/albumsSlice.ts (3)
  • setSelectedAlbum (48-50)
  • setAlbumImages (52-54)
  • clearAlbumImages (72-74)
frontend/src/features/imageSlice.ts (3)
  • setImages (18-20)
  • clearImages (39-42)
  • setCurrentViewIndex (22-34)
frontend/src/components/Media/ImageCard.tsx (1)
  • ImageCard (19-109)
frontend/src/components/Media/MediaView.tsx (1)
  • MediaView (23-214)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1)
  • AddImagesToAlbumDialog (23-210)
frontend/src/pages/Album/Album.tsx (7)
frontend/src/features/albumSelectors.ts (1)
  • selectAlbums (5-5)
frontend/src/features/albumsSlice.ts (2)
  • removeAlbum (38-46)
  • setAlbums (21-23)
frontend/src/types/Album.ts (1)
  • Album (1-10)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (2)
  • getAllAlbums (15-20)
  • deleteAlbum (67-72)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
🪛 GitHub Actions: PR Check
backend/app/routes/albums.py

[error] 1-1: API response for /albums endpoints is missing the 'is_hidden' field in album objects. Tests expect this field in the JSON response but it is not present.


[error] 1-1: db_get_all_albums() is being called without a boolean argument, while tests expect it to be called with a 'show_hidden' flag (True/False). This mismatch causes test failures.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (11)
frontend/src/components/EmptyStates/EmptyAlbumsState.tsx (1)

1-2: LGTM!

The icon imports are appropriate for the empty state component.

frontend/src/api/apiEndpoints.ts (1)

33-46: LGTM! Clean endpoint definitions.

The album endpoints follow the established pattern and RESTful conventions. The implementation is consistent with existing endpoint groups in the file.

frontend/src/app/store.ts (1)

9-20: LGTM! Proper Redux integration.

The albums reducer is correctly imported and integrated into the store configuration following the established pattern.

frontend/src/components/Albums/EditAlbumDialog.tsx (2)

92-112: Validation logic handles lock state transitions correctly.

The validation properly distinguishes between:

  • Locking an unlocked album (requires new password)
  • Modifying a locked album (requires current password)
  • Changing password on locked album (current password required, new password optional)

22-301: Well-implemented edit dialog with proper password state management.

The component correctly handles the complex logic for editing locked/unlocked albums, including:

  • Appropriate validation for different lock states
  • Clear UI feedback with contextual labels
  • Proper cleanup on close
frontend/src/components/Albums/AlbumCard.tsx (2)

26-55: Robust image handling with proper error fallback.

The component correctly:

  • Selects theme-appropriate placeholders
  • Uses Tauri's convertFileSrc for local file paths
  • Prevents infinite error loops by setting img.onerror = null

92-103: Clean presentation with proper text handling.

The info section correctly:

  • Truncates long album names
  • Clamps description to 2 lines
  • Pluralizes "photo/photos" appropriately
frontend/src/pages/Album/AlbumDetail.tsx (2)

215-221: Proper cleanup prevents state leaks.

The cleanup effect correctly clears all album-related state on unmount, preventing stale data from persisting.


43-437: Comprehensive album detail page with good UX patterns.

The component effectively handles:

  • Password-protected album access
  • Multiple viewing modes (normal, selection, media viewer)
  • Bulk operations with clear visual feedback
  • Context menus for per-image actions
  • Proper state cleanup on unmount
frontend/src/api/api-functions/albums.ts (1)

1-151: LGTM! Well-structured API layer.

The API functions are well-documented, properly typed, and use appropriate HTTP methods. The consistent error handling pattern and clear separation of concerns make this module easy to maintain.

backend/app/database/albums.py (1)

65-82: LGTM! Well-implemented migration.

The migration function properly checks for column existence before altering the table, preventing errors on repeated runs. Good defensive programming.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/tests/test_albums.py (2)

467-488: Fix inconsistent mock return tuple shape.

Line 477 uses tuple(mock_db_album.values()) which creates a 5-element tuple from the fixture dict. However, other tests throughout this file explicitly construct 6-element tuples that include image_count as the last element. This inconsistency could cause test failures if db_get_album expects to return 6 elements.

Apply this diff to use a consistent 6-element tuple:

-        with patch("app.routes.albums.db_get_album") as mock_get, patch(
-            "app.routes.albums.db_remove_images_from_album"
-        ) as mock_remove_bulk:
-            mock_get.return_value = tuple(mock_db_album.values())
+        album_tuple = (
+            mock_db_album["album_id"],
+            mock_db_album["album_name"],
+            mock_db_album["description"],
+            int(mock_db_album["is_locked"]),
+            mock_db_album["password_hash"],
+            0,  # image_count
+        )
+
+        with patch("app.routes.albums.db_get_album") as mock_get, patch(
+            "app.routes.albums.db_remove_images_from_album"
+        ) as mock_remove_bulk:
+            mock_get.return_value = album_tuple
             response = client.request(
                 "DELETE", f"/albums/{album_id}/images", json=image_ids_to_remove
             )

99-105: Add missing password_hash field to the 6-element tuple for db_get_album_by_name mock.

The db_get_album_by_name function's SQL query selects 6 fields (album_id, album_name, description, is_locked, password_hash, cover_image_path), but this mock returns only 5 elements. Update the mock to include the password_hash field as the 5th element:

mock_get_by_name.return_value = (
    "existing-id",
    "Existing Album",
    "desc",
    0,
    None,  # password_hash
    None,  # cover_image_path
)
🧹 Nitpick comments (1)
backend/tests/test_albums.py (1)

149-185: Consider renaming test function for consistency.

The test function name still contains "hidden" (test_get_all_albums_include_hidden), while the docstring and implementation have been updated to use "locked" terminology. For consistency, consider renaming this to test_get_all_albums_include_locked.

-    def test_get_all_albums_include_hidden(self, mock_db_album, mock_db_locked_album):
+    def test_get_all_albums_include_locked(self, mock_db_album, mock_db_locked_album):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afcecea and 5841ada.

📒 Files selected for processing (1)
  • backend/tests/test_albums.py (14 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/tests/test_albums.py (1)
backend/tests/test_folders.py (1)
  • client (76-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Backend Tests
🔇 Additional comments (3)
backend/tests/test_albums.py (3)

23-42: LGTM! Fixture updates are correct.

The fixtures have been correctly updated to reflect the migration from is_hidden to is_locked, and the naming change from mock_db_hidden_album to mock_db_locked_album is appropriate.


53-352: Test updates for CRUD operations look good.

The test methods in TestAlbumRoutes have been appropriately updated to reflect the migration from is_hidden to is_locked. The parametrized tests provide good coverage of different scenarios (public/locked albums, password validation), and most mock return tuples consistently use the 6-element format with cover_image_path and image_count fields.


355-466: Test updates for image management operations look good.

The test methods in TestAlbumImageManagement correctly use the updated 6-element tuple format for mock album data, ensuring consistency with the new album schema that includes is_locked, cover_image_path, and image_count fields.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
backend/app/database/albums.py (1)

107-118: LGTM! Past issue with unused parameter resolved.

The removal of the show_hidden/show_locked parameter addresses the previous review comment about an ignored parameter. The function now has clear, unambiguous behavior—it always returns all albums.

backend/app/routes/albums.py (2)

128-182: LGTM! Update endpoint correctly refactored.

The endpoint correctly uses is_locked throughout, and there's no duplicate route decorator issue (which was mentioned in a previous review). The password validation logic for locked albums is properly maintained.


376-430: LGTM! Cover image endpoint is well-implemented.

The new endpoint has comprehensive validation:

  1. Album existence check
  2. Image membership verification (image must be in the album)
  3. Image existence validation

The use of db_get_image_path maintains proper architectural separation (addressing a previous review comment about inline database queries).

🧹 Nitpick comments (4)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (2)

34-44: Remove redundant state duplication.

The allImages state (line 32) unnecessarily duplicates imagesData.data. This adds complexity, extra re-renders, and a type assertion that could be unsafe.

Apply this diff to use imagesData.data directly:

-  const [allImages, setAllImages] = useState<Image[]>([]);

   const { data: imagesData, isLoading } = usePictoQuery({
     queryKey: ['images'],
     queryFn: () => fetchAllImages(),
     enabled: isOpen,
   });

-  useEffect(() => {
-    if (imagesData?.data) {
-      setAllImages(imagesData.data as Image[]);
-    }
-  }, [imagesData]);

Then update line 104 to use imagesData?.data directly:

-  const filteredImages = allImages.filter((image) => {
+  const filteredImages = (imagesData?.data as Image[] || []).filter((image) => {

46-96: Simplify by removing manual loader dispatches.

The component uses both manual loader dispatches (showLoader/hideLoader at lines 50, 61, 94) and the isPending flag from the mutation. This is redundant—isPending already provides the loading state needed for UI feedback.

Remove the manual loader dispatches and rely solely on isPending:

   const handleSubmit = () => {
     if (selectedImages.size === 0) {
       dispatch(
         showInfoDialog({
           title: 'No Images Selected',
           message: 'Please select at least one image to add to the album.',
           variant: 'info',
         }),
       );
       return;
     }

-    dispatch(showLoader('Adding images to album...'));
     addImagesMutate({ image_ids: Array.from(selectedImages) });
   };

And remove hideLoader calls from the mutation callbacks (lines 50, 61):

   const { mutate: addImagesMutate, isPending } = usePictoMutation({
     mutationFn: (data: { image_ids: string[] }) =>
       addImagesToAlbum(albumId, data),
     onSuccess: () => {
-      dispatch(hideLoader());
       dispatch(
         showInfoDialog({
           title: 'Success',
           message: `Added ${selectedImages.size} image${selectedImages.size > 1 ? 's' : ''} to album!`,
           variant: 'info',
         }),
       );
       handleClose();
     },
     onError: (error: any) => {
-      dispatch(hideLoader());
       dispatch(
         showInfoDialog({
           title: 'Error',
           message: error?.message || 'Failed to add images. Please try again.',
           variant: 'error',
         }),
       );
     },
   });

The button states at lines 200 and 206 already use isPending correctly.

backend/app/database/albums.py (1)

220-322: Consider using the context manager consistently.

While not critical, there's an inconsistency in connection handling: db_update_album_cover_image and db_get_image_path use manual connection management, while some other functions (e.g., db_delete_album, db_add_images_to_album) use the get_db_connection() context manager defined at the top of the file. The context manager provides retry logic and better error handling.

Consider refactoring these functions to use the context manager:

 def db_update_album_cover_image(album_id: str, cover_image_path: str):
     """Update the cover image path for an album"""
-    conn = sqlite3.connect(DATABASE_PATH)
-    cursor = conn.cursor()
-    try:
+    with get_db_connection() as conn:
+        cursor = conn.cursor()
         cursor.execute(
             "UPDATE albums SET cover_image_path = ? WHERE album_id = ?",
             (cover_image_path, album_id),
         )
-        conn.commit()
-    finally:
-        conn.close()

Apply similar changes to db_get_image_path and other functions using manual connection management.

backend/app/routes/albums.py (1)

43-45: Consider optimizing image count queries.

The current implementation performs N+1 queries: one to fetch all albums, then one per album to count images. For large album collections, this could be slow.

Consider adding a database function that returns albums with image counts in a single query:

def db_get_all_albums_with_counts():
    """Get all albums with their image counts in a single query"""
    conn = sqlite3.connect(DATABASE_PATH)
    cursor = conn.cursor()
    try:
        cursor.execute(
            """
            SELECT a.album_id, a.album_name, a.description, a.is_locked, 
                   a.password_hash, a.cover_image_path, COUNT(ai.image_id) as image_count
            FROM albums a
            LEFT JOIN album_images ai ON a.album_id = ai.album_id
            GROUP BY a.album_id
            """
        )
        return cursor.fetchall()
    finally:
        conn.close()

This reduces the operation from O(n) queries to O(1).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5841ada and 67e9937.

📒 Files selected for processing (8)
  • backend/app/database/albums.py (11 hunks)
  • backend/app/routes/albums.py (9 hunks)
  • backend/app/schemas/album.py (3 hunks)
  • backend/tests/test_albums.py (14 hunks)
  • frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/AlbumPasswordDialog.tsx (1 hunks)
  • frontend/src/features/albumSelectors.ts (1 hunks)
  • frontend/src/types/Album.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • frontend/src/features/albumSelectors.ts
  • frontend/src/components/Albums/AlbumPasswordDialog.tsx
  • backend/app/schemas/album.py
  • frontend/src/types/Album.ts
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (7)
frontend/src/types/Album.ts (1)
  • AddImagesToAlbumDialogProps (66-71)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/api/api-functions/albums.ts (1)
  • addImagesToAlbum (79-88)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
backend/app/routes/albums.py (2)
backend/app/schemas/album.py (5)
  • SetCoverImageRequest (60-61)
  • GetAlbumsResponse (69-71)
  • Album (6-12)
  • ErrorResponse (94-97)
  • SuccessResponse (89-91)
backend/app/database/albums.py (5)
  • db_update_album_cover_image (220-231)
  • db_get_image_path (313-322)
  • db_get_all_albums (107-118)
  • db_get_album_images (234-244)
  • db_get_album (135-146)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (18)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (6)

1-21: LGTM!

Imports are well-organized and all dependencies are properly declared.


23-32: LGTM!

Component props and state initialization are correct. Using a Set for selectedImages is an efficient choice for fast lookups and toggles.


72-102: LGTM!

Handler functions are correctly implemented:

  • handleImageToggle properly creates a new Set for immutability
  • handleSubmit validates selection before proceeding
  • handleClose properly resets all local state before closing

104-108: LGTM!

Filtering logic is correct with proper handling of edge cases (undefined from .pop() via || ''). Case-insensitive search provides good UX.


161-170: Past review feedback addressed—theme-aware placeholders implemented correctly.

The previous review comment about using non-existent /placeholder.svg has been resolved. The current implementation correctly uses theme-aware placeholders (/placeholder-album.svg for dark mode, /placeholder-album-light.svg for light mode) via window.matchMedia, aligning with the project's existing assets.


110-215: LGTM!

UI structure and rendering logic are well-implemented:

  • Proper loading, empty, and error states
  • Accessible search with icon
  • Responsive grid layout with visual selection feedback
  • Clear user feedback with selected count and dynamic button text
  • Buttons correctly disabled based on pending state and selection
backend/tests/test_albums.py (4)

23-42: LGTM! Fixture renaming aligns with schema changes.

The fixture renaming from mock_db_hidden_album to mock_db_locked_album and the field update from is_hidden to is_locked correctly reflect the backend schema changes.


53-68: LGTM! Test parameterization covers locked and unlocked scenarios.

The test cases correctly exercise both is_locked=False (public) and is_locked=True (password-protected) album creation flows.


119-171: LGTM! Mock return data correctly reflects database schema changes.

The addition of cover_image_path and image_count placeholders to mock return tuples aligns with the updated database layer, and all is_hidden references have been correctly replaced with is_locked.


329-353: LGTM! Test expectations correctly updated for schema changes.

The test correctly constructs album tuples with int(mock_db_album["is_locked"]) for SQLite compatibility and includes the image_count placeholder, consistent with the database layer changes.

backend/app/database/albums.py (4)

42-62: LGTM! Album table schema correctly updated.

The schema now includes is_locked, password_hash, and cover_image_path fields, aligning with the feature requirements for locked albums and cover image management.


149-211: LGTM! Parameter renaming and SQL updates are consistent.

The renaming of is_hidden to is_locked is correctly applied in both db_insert_album and db_update_album, with proper boolean-to-integer conversion for SQLite compatibility.


220-231: LGTM! Cover image update function is straightforward.

The db_update_album_cover_image function correctly updates the cover image path for an album.


313-322: LGTM! Image path retrieval function is correct.

The db_get_image_path function appropriately queries the images table to retrieve the file path for a given image ID.

backend/app/routes/albums.py (4)

1-31: LGTM! Imports correctly updated for new functionality.

The addition of SetCoverImageRequest, db_update_album_cover_image, and db_get_image_path supports the new cover image endpoint and addresses the previous inline database query issue.


36-57: LGTM! Function correctly updated for schema changes.

The removal of the parameter and the addition of image count and cover image path support align with the backend changes. The safe indexing album[5] if len(album) > 5 else None handles potential schema migration cases gracefully.


91-125: LGTM! Album detail endpoint correctly updated.

The endpoint properly incorporates image count calculation and cover image path, with consistent error handling and safe field access.


211-267: LGTM! Image retrieval endpoint correctly updated.

The endpoint properly checks is_locked status and requires password authentication for locked albums, with clear error messages.

Comment on lines +65 to +82
def db_migrate_add_cover_image_column() -> None:
"""Add cover_image_path column to existing albums table if it doesn't exist"""
conn = None
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

# Check if column exists
cursor.execute("PRAGMA table_info(albums)")
columns = [column[1] for column in cursor.fetchall()]

if "cover_image_path" not in columns:
cursor.execute("ALTER TABLE albums ADD COLUMN cover_image_path TEXT")
conn.commit()
print("Added cover_image_path column to albums table")
finally:
if conn is not None:
conn.close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify migration function is invoked.

The migration function logic is correct, but ensure it's being called during application initialization or in a migration script to actually apply the schema change to existing databases.

Run this script to check if the migration function is invoked:


🏁 Script executed:

#!/bin/bash
# Check if db_migrate_add_cover_image_column is called anywhere
rg -n "db_migrate_add_cover_image_column" --type=py -C 3

Length of output: 421



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for migration-related patterns in initialization and main files
rg -n "(migrate|initialization|startup)" --type=py -C 2 backend/app/__init__.py backend/app/database/__init__.py backend/app/main.py 2>/dev/null || echo "No direct matches"

# Also check if there's any file that imports this migration function
rg -n "from.*albums.*import|import.*db_migrate" --type=py

Length of output: 423



---

🏁 Script executed:

```shell
#!/bin/bash
# Check where db_create functions are called in main.py to understand the initialization flow
sed -n '15,50p' backend/main.py

Length of output: 1412



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the full lifespan function to see complete initialization
sed -n '40,70p' backend/main.py

Length of output: 921


Add migration function to application startup sequence.

The db_migrate_add_cover_image_column function is defined but never called, so the cover_image_path column won't be added to existing databases. Import and invoke it during application initialization in backend/main.py within the lifespan context manager, after db_create_albums_table() is called (around line 54).

🤖 Prompt for AI Agents
In backend/main.py around line 54 inside the lifespan context manager, the
migration function db_migrate_add_cover_image_column from
backend.app.database.albums is not invoked; import
db_migrate_add_cover_image_column at the top of the file and call it immediately
after the existing db_create_albums_table() invocation so the migration runs on
startup for existing databases (keep it synchronous, handle/let exceptions
propagate as with db_create_albums_table()).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/src/components/Albums/EditAlbumDialog.tsx (1)

38-40: Prefer explicit typing over any for type safety.

The mutation function parameter data is typed as any, which bypasses TypeScript's type checking. Consider using the UpdateAlbumRequest type instead.

Apply this diff to improve type safety:

-  const { mutate: updateAlbumMutate, isPending } = usePictoMutation({
-    mutationFn: ({ albumId, data }: { albumId: string; data: any }) =>
-      updateAlbum(albumId, data),
+  const { mutate: updateAlbumMutate, isPending } = usePictoMutation({
+    mutationFn: ({ albumId, data }: { albumId: string; data: UpdateAlbumRequest }) =>
+      updateAlbum(albumId, data),

You'll also need to import the type:

import { UpdateAlbumRequest } from '@/types/Album';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67e9937 and 634140f.

📒 Files selected for processing (3)
  • frontend/src/components/Albums/CreateAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/EditAlbumDialog.tsx (1 hunks)
  • frontend/src/features/albumSelectors.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/components/Albums/CreateAlbumDialog.tsx
  • frontend/src/features/albumSelectors.ts
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/components/Albums/EditAlbumDialog.tsx (5)
frontend/src/types/Album.ts (1)
  • EditAlbumDialogProps (46-51)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (1)
  • updateAlbum (52-61)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Backend Tests
🔇 Additional comments (3)
frontend/src/components/Albums/EditAlbumDialog.tsx (3)

80-90: LGTM!

The form population logic correctly pre-fills the form fields from the album prop while resetting password fields to empty strings for security. The effect dependency on [album] is appropriate.


92-112: LGTM!

The validation logic correctly handles all scenarios:

  • Requires name field
  • Requires current password when modifying a locked album
  • Requires new password when locking a previously unlocked album

The use of optional chaining (album?.is_locked) safely handles null cases.


205-281: LGTM!

The password fields logic is well-implemented:

  • Current password is shown and required only for albums that were originally locked
  • New password field appears when the lock toggle is enabled
  • Labels and placeholders are contextual and user-friendly
  • Required indicators correctly match the validation rules

The UX clearly communicates when passwords are optional vs. required.

Comment on lines 60 to 82
onSuccess: (_, albumId) => {
dispatch(hideLoader());
dispatch(removeAlbum(albumId as string));
dispatch(
showInfoDialog({
title: 'Success',
message: 'Album deleted successfully!',
variant: 'info',
}),
);
},
onError: (error: any) => {
dispatch(hideLoader());
dispatch(
showInfoDialog({
title: 'Error',
message:
error?.message || 'Failed to delete album. Please try again.',
variant: 'error',
}),
);
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer this file: frontend/src/hooks/useUserPreferences.tsx

Utilize the useMutationFeedback function to handle the post-API call logic.

Make such change everywhere we are making album API calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copilot AI review requested due to automatic review settings February 15, 2026 13:30
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a complete Albums feature for the Pictopy application, adding both backend API endpoints and a fully functional frontend UI. The implementation allows users to create, edit, delete albums, manage album images, set cover images, and protect albums with passwords.

Changes:

  • Implemented complete album CRUD operations with locking/password protection
  • Built comprehensive frontend UI with dialogs for album management and image selection
  • Added Redux state management with albumsSlice and memoized selectors
  • Updated backend schema from is_hidden to is_locked with cover image support

Reviewed changes

Copilot reviewed 22 out of 24 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/src/types/Album.ts Updated type definitions replacing is_hidden with is_locked, added cover_image_path and image_count
frontend/src/routes/AppRoutes.tsx Added Album and AlbumDetail route components
frontend/src/pages/Album/Album.tsx Main albums list page with create, edit, delete, and password dialog functionality
frontend/src/pages/Album/AlbumDetail.tsx Album detail page showing images with add/remove and cover image selection
frontend/src/features/albumsSlice.ts Redux slice for albums state management with 11 actions
frontend/src/features/albumSelectors.ts Memoized selectors for album state
frontend/src/constants/routes.ts Added ALBUM_DETAIL route constant
frontend/src/components/EmptyStates/EmptyAlbumsState.tsx Empty state component for albums list
frontend/src/components/Albums/EditAlbumDialog.tsx Dialog for editing album with lock management
frontend/src/components/Albums/DeleteConfirmDialog.tsx Confirmation dialog for album deletion
frontend/src/components/Albums/CreateAlbumDialog.tsx Dialog for creating new albums
frontend/src/components/Albums/AlbumPasswordDialog.tsx Password prompt for locked albums
frontend/src/components/Albums/AlbumCard.tsx Album card component with cover image and lock icon
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx Dialog for selecting and adding images to album
frontend/src/app/store.ts Integrated albums reducer into Redux store
frontend/src/api/apiEndpoints.ts Added album API endpoints configuration
frontend/src/api/api-functions/index.ts Exported album API functions
frontend/src/api/api-functions/albums.ts Comprehensive album API functions for all operations
frontend/public/placeholder-album.svg Dark theme placeholder for albums without cover
frontend/public/placeholder-album-light.svg Light theme placeholder for albums without cover
backend/tests/test_albums.py Updated tests to use is_locked instead of is_hidden
backend/app/schemas/album.py Updated schemas with is_locked, cover_image_path, and image_count
backend/app/routes/albums.py Updated routes with is_locked semantics and cover image endpoint
backend/app/database/albums.py Updated database functions and added migration function for cover_image_path

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 42 to 56
for album in albums:
# Get image count for each album
image_ids = db_get_album_images(album[0])
image_count = len(image_ids)

album_list.append(
Album(
album_id=album[0],
album_name=album[1],
description=album[2] or "",
is_hidden=bool(album[3]),
is_locked=bool(album[3]),
cover_image_path=album[5] if len(album) > 5 else None,
image_count=image_count,
)
)
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_albums endpoint calls db_get_album_images() for every album in a loop (line 44), which results in N+1 database queries. For users with many albums, this could cause significant performance degradation. Consider optimizing this by either: (1) adding a database query that counts images per album in a single query using a JOIN or GROUP BY, or (2) caching the image counts in the albums table itself and updating it when images are added/removed.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +82
def db_migrate_add_cover_image_column() -> None:
"""Add cover_image_path column to existing albums table if it doesn't exist"""
conn = None
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

# Check if column exists
cursor.execute("PRAGMA table_info(albums)")
columns = [column[1] for column in cursor.fetchall()]

if "cover_image_path" not in columns:
cursor.execute("ALTER TABLE albums ADD COLUMN cover_image_path TEXT")
conn.commit()
print("Added cover_image_path column to albums table")
finally:
if conn is not None:
conn.close()
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration function db_migrate_add_cover_image_column is defined but there's no evidence it's being called during application startup in the modified code. This migration is necessary for existing databases that don't have the cover_image_path column. For backward compatibility with existing databases, this function should be called after table creation but before the application starts using album functionality.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +21

export const EditAlbumDialog: React.FC<EditAlbumDialogProps> = ({
album,
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports showInfoDialog, showLoader, and hideLoader are imported but never used in this component. The useMutationFeedback hook already handles loading and info dialog display. Remove these unused imports to keep the code clean.

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +163
Protect your album with a password
</p>
</div>
<Switch
id="locked"
checked={formData.is_locked}
onCheckedChange={(checked) =>
setFormData({ ...formData, is_locked: checked })
}
/>
</div>

{/* Password Field (shown only if locked) */}
{formData.is_locked && (
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The htmlFor attribute on the Label references "hidden" but the Switch has id="locked". This mismatch breaks the label-input association. Change the label's htmlFor from "hidden" to "locked" to match the Switch's id attribute.

Copilot uses AI. Check for mistakes.

const handlePasswordSubmit = (password: string) => {
if (albumToAccess) {
navigate(`/albums/${albumToAccess.id}`, { state: { password } });
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing the album password through navigation state (line 117) may expose it in browser history or debugging tools. Consider implementing a more secure approach such as storing the password in memory using a ref or session storage with appropriate cleanup, or re-prompting for the password when needed on the detail page. The current approach could allow the password to be inspected through browser developer tools.

Suggested change
navigate(`/albums/${albumToAccess.id}`, { state: { password } });
try {
sessionStorage.setItem(`album_password_${albumToAccess.id}`, password);
} catch (e) {
// If sessionStorage is unavailable, proceed without storing the password.
}
navigate(`/albums/${albumToAccess.id}`);

Copilot uses AI. Check for mistakes.
Comment on lines 47 to 104
const {
data: albumsData,
successData,
isLoading,
isSuccess,
isError,
refetch,
} = usePictoQuery({
queryKey: ['albums'],
queryFn: () => getAllAlbums(),
});

const deleteAlbumMutation = usePictoMutation({
mutationFn: deleteAlbum,
});

useMutationFeedback(deleteAlbumMutation, {
loadingMessage: 'Deleting album...',
successTitle: 'Success',
successMessage: 'Album deleted successfully!',
errorTitle: 'Error',
errorMessage: 'Failed to delete album. Please try again.',
onSuccess: () => {
if (albumToDelete) {
dispatch(removeAlbum(albumToDelete.id));
}
},
});

useEffect(() => {
if (isLoading) {
dispatch(showLoader('Loading albums...'));
} else if (isError) {
dispatch(hideLoader());
dispatch(
showInfoDialog({
title: 'Error',
message: 'Failed to load albums. Please try again later.',
variant: 'error',
}),
);
} else if (isSuccess && albumsData) {
const responseData = albumsData as any;
const backendAlbums = (responseData?.albums || []) as any[];
const albumsList = backendAlbums.map((album: any) => ({
id: album.album_id,
name: album.album_name,
description: album.description || '',
is_locked: Boolean(album.is_locked),
cover_image_path: album.cover_image_path,
image_count: album.image_count || 0,
created_at: album.created_at || new Date().toISOString(),
updated_at: album.updated_at || new Date().toISOString(),
})) as Album[];
dispatch(setAlbums(albumsList));
dispatch(hideLoader());
}
}, [albumsData, successData, isSuccess, isError, isLoading, dispatch]);
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The successData variable is destructured from the query result but is never used in the component. It's only included in the useEffect dependency array. Consider removing it from the destructuring assignment and the dependency array since albumsData alone provides all the necessary data for the effect.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +21

export const CreateAlbumDialog: React.FC<CreateAlbumDialogProps> = ({
isOpen,
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports showInfoDialog, showLoader, and hideLoader are imported but never used in this component. The useMutationFeedback hook already handles loading and info dialog display. Remove these unused imports to keep the code clean.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +18
import { showInfoDialog } from '@/features/infoDialogSlice';
import { useMutationFeedback } from '@/hooks/useMutationFeedback';
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports showInfoDialog, showLoader, and hideLoader are imported but never used in this component. The useMutationFeedback hook already handles loading and info dialog display for the mutations, while showInfoDialog for the empty selection case is the only used import. Remove showLoader and hideLoader imports.

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +160
const img = e.target as HTMLImageElement;
img.onerror = null;
const placeholder = window.matchMedia(
'(prefers-color-scheme: dark)',
).matches
? '/placeholder-album.svg'
: '/placeholder-album-light.svg';
img.src = placeholder;
}}
/>
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placeholder image selection uses window.matchMedia('(prefers-color-scheme: dark)') to detect the theme, which checks the system preference rather than the app's theme state. This is inconsistent with AlbumCard.tsx which uses the useTheme hook. For consistent behavior, use useTheme hook from @/contexts/ThemeContext instead of window.matchMedia. This ensures the placeholder matches the user's selected theme in the app, not just the system preference.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@frontend/src/components/Albums/AddImagesToAlbumDialog.tsx`:
- Line 18: Remove the unused imports hideLoader and showLoader from the top of
AddImagesToAlbumDialog.tsx to resolve TS6192 and ESLint errors; locate the
import line that currently imports hideLoader and showLoader and delete them so
only the actually used symbols (e.g., other imports and the useMutationFeedback
hook) remain, ensuring the file no longer references those unused symbols.

In `@frontend/src/components/Albums/CreateAlbumDialog.tsx`:
- Around line 19-20: Remove the unused imports and variable in
CreateAlbumDialog: delete the imported symbols showInfoDialog, showLoader,
hideLoader and remove the unused dispatch variable (from useDispatch) since
useMutationFeedback already handles loader and info dialog behavior; update the
import list at the top of CreateAlbumDialog.tsx to omit those three imports and
remove the const dispatch = useDispatch() declaration so ESLint warnings are
resolved while leaving useMutationFeedback and the component logic intact.
- Around line 152-158: The Label's htmlFor value ("hidden") doesn't match the
Switch id ("locked") in CreateAlbumDialog, so clicking the label won't toggle
the switch; update the Label htmlFor to "locked" (or change the Switch id to
"hidden") so they match, and ensure the underlying prop/state name (is_locked /
is_hidden) used in CreateAlbumDialog (and the similar case in EditAlbumDialog)
is consistent with the updated id to preserve accessibility and wiring to the
form control.

In `@frontend/src/components/Albums/EditAlbumDialog.tsx`:
- Around line 19-20: Remove unused imports and variable: delete showInfoDialog,
showLoader, hideLoader from the import list and remove the useDispatch import
and the local dispatch variable in EditAlbumDialog.tsx since useMutationFeedback
now handles loader/dialog dispatching; specifically remove references to the
symbols showInfoDialog, showLoader, hideLoader, useDispatch and the dispatch
variable to eliminate ESLint unused-import/variable warnings.
- Line 152: The Label's htmlFor ("hidden") doesn't match the Switch's id
("locked") and doesn't reflect the is_locked semantics; update them to match and
be descriptive (e.g., change Label htmlFor="hidden" to htmlFor="locked" or
rename both to "is_locked") so the <Label htmlFor="..."> pairs with <Switch
id="..."> and the form field (formData.is_locked / is_locked prop) consistently
uses that identifier; ensure any name/checked bindings for the Switch also use
the same key (is_locked or locked) throughout EditAlbumDialog.

In `@frontend/src/pages/Album/Album.tsx`:
- Around line 131-137: confirmDelete currently nulls out albumToDelete before
the async mutation completes, causing onSuccess handlers (from
useMutationFeedback) to see albumToDelete === null and fail to
dispatch(removeAlbum(...)). Fix by capturing the id (e.g., const id =
albumToDelete?.id) before calling deleteAlbumMutation.mutate(id) and use that id
in the mutation feedback/onSuccess to dispatch(removeAlbum(id)) and only clear
setAlbumToDelete(null) and close the dialog after onSuccess (or perform those
cleanup actions inside onSuccess) so the deleted album reference remains
available to the feedback callback.
- Around line 139-154: handleRefresh currently assumes refetch() throws on
failure; change it to await the refetch() result and check its properties (e.g.,
result.isError or result.error) instead of relying on catch — always call
dispatch(hideLoader()) after refetch returns, and if result.isError or
result.error is present call dispatch(showInfoDialog(...)) with the error
message/variant 'error'; update the function name handleRefresh and the refetch
call sites to use the returned result object from refetch().
🧹 Nitpick comments (6)
frontend/src/pages/Album/AlbumDetail.tsx (4)

131-152: Extensive as any casting bypasses type safety for backend data transformation.

Consider defining a typed interface for the backend album response shape (e.g., BackendAlbumResponse) and using it in place of the double as any cast. This would catch field renames or missing properties at compile time rather than silently producing undefined.


168-178: O(n×m) image filtering — use a Set for the lookup.

imageIds.includes(img.id) inside allImages.filter(...) is O(n×m). For large libraries this degrades quickly. Convert imageIds to a Set first.

Also, fetching the entire image library (fetchAllImages) just to hydrate album images is expensive. Consider a backend endpoint that returns full image objects for an album directly.

Quick fix for the Set lookup
-      const albumImages = allImages.filter((img) => imageIds.includes(img.id));
+      const imageIdSet = new Set(imageIds);
+      const albumImages = allImages.filter((img) => imageIdSet.has(img.id));

62-90: Sequential waterfall: album → images & allImages queries are chained via enabled: !!album.

Both the album-images and all-images queries depend on album being set in Redux (via the first useEffect), which only runs after the album query resolves. This creates a request waterfall (album fetch → render → effect → re-render → images fetch). If you already have albumId from the URL, you could enable the images queries with just !!albumId, removing the dependency on album being fully hydrated in Redux.


144-147: Fallback timestamps (new Date().toISOString()) are misleading.

If the backend doesn't return created_at/updated_at, fabricating a current timestamp silently hides missing data and may cause incorrect sorting or display. Consider using null / undefined with an appropriate type to surface the absence.

frontend/src/pages/Album/Album.tsx (1)

88-103: Loader persists during initial render when albums is empty.

While the useEffect dispatches showLoader during isLoading, the component body still renders. If albums in Redux starts as [], <EmptyAlbumsState /> briefly flashes behind the loader overlay. Consider a guard (e.g., if (isLoading) return null;) or checking isLoading before rendering the album grid.

frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1)

154-158: prefers-color-scheme may not match the app's active theme.

If the app uses its own theme toggle (e.g., a dark/light class on <html>), window.matchMedia('(prefers-color-scheme: dark)') reflects the OS preference, not the app state. Consider checking the same source of truth the rest of the app uses (e.g., a dark class on document.documentElement).

import { usePictoMutation, usePictoQuery } from '@/hooks/useQueryExtension';
import { addImagesToAlbum, fetchAllImages } from '@/api/api-functions';
import { showInfoDialog } from '@/features/infoDialogSlice';
import { hideLoader, showLoader } from '@/features/loaderSlice';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove unused import — causes both TypeScript error (TS6192) and ESLint failure.

hideLoader and showLoader are never used in this file (the useMutationFeedback hook handles loader state internally). This triggers a TypeScript compile error and ESLint warnings that fail the pipeline.

Proposed fix
-import { hideLoader, showLoader } from '@/features/loaderSlice';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { hideLoader, showLoader } from '@/features/loaderSlice';
🧰 Tools
🪛 GitHub Actions: PR Check

[warning] 18-18: eslint: 'hideLoader' is defined but never used. (no-unused-vars)


[warning] 18-18: eslint: 'showLoader' is defined but never used. (no-unused-vars)

🪛 GitHub Actions: PR Check Build

[error] 18-18: TypeScript error TS6192: All imports in import declaration are unused.

🪛 GitHub Check: Linting

[warning] 18-18:
'showLoader' is defined but never used


[warning] 18-18:
'hideLoader' is defined but never used

🤖 Prompt for AI Agents
In `@frontend/src/components/Albums/AddImagesToAlbumDialog.tsx` at line 18, Remove
the unused imports hideLoader and showLoader from the top of
AddImagesToAlbumDialog.tsx to resolve TS6192 and ESLint errors; locate the
import line that currently imports hideLoader and showLoader and delete them so
only the actually used symbols (e.g., other imports and the useMutationFeedback
hook) remain, ensuring the file no longer references those unused symbols.

Comment on lines 19 to 20
import { showInfoDialog } from '@/features/infoDialogSlice';
import { hideLoader, showLoader } from '@/features/loaderSlice';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove unused imports and variable — same pipeline failure as EditAlbumDialog.

showInfoDialog, showLoader, hideLoader (lines 19–20) and the dispatch variable (line 28) are unused — useMutationFeedback handles these internally. This triggers ESLint warnings that fail the build.

Proposed fix
-import { showInfoDialog } from '@/features/infoDialogSlice';
-import { hideLoader, showLoader } from '@/features/loaderSlice';
-import { useDispatch } from 'react-redux';
 }) => {
-  const dispatch = useDispatch();
   const [formData, setFormData] = useState({

Also applies to: 27-28

🧰 Tools
🪛 GitHub Actions: PR Check

[warning] 19-19: eslint: 'showInfoDialog' is defined but never used. (no-unused-vars)


[warning] 20-20: eslint: 'hideLoader' is defined but never used. (no-unused-vars)


[warning] 20-20: eslint: 'showLoader' is defined but never used. (no-unused-vars)

🪛 GitHub Check: Linting

[warning] 20-20:
'showLoader' is defined but never used


[warning] 20-20:
'hideLoader' is defined but never used


[warning] 19-19:
'showInfoDialog' is defined but never used

🤖 Prompt for AI Agents
In `@frontend/src/components/Albums/CreateAlbumDialog.tsx` around lines 19 - 20,
Remove the unused imports and variable in CreateAlbumDialog: delete the imported
symbols showInfoDialog, showLoader, hideLoader and remove the unused dispatch
variable (from useDispatch) since useMutationFeedback already handles loader and
info dialog behavior; update the import list at the top of CreateAlbumDialog.tsx
to omit those three imports and remove the const dispatch = useDispatch()
declaration so ESLint warnings are resolved while leaving useMutationFeedback
and the component logic intact.

Comment on lines 152 to 158
<Label htmlFor="hidden">Lock Album</Label>
<p className="text-muted-foreground text-sm">
Protect your album with a password
</p>
</div>
<Switch
id="locked"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mismatched htmlFor/id: Label says "hidden" but the Switch id is "locked".

Same issue as in EditAlbumDialog. The <Label htmlFor="hidden"> (line 152) doesn't match <Switch id="locked"> (line 158), so clicking the label won't toggle the switch. This also appears to be a leftover from the is_hiddenis_locked rename.

Proposed fix
-              <Label htmlFor="hidden">Lock Album</Label>
+              <Label htmlFor="locked">Lock Album</Label>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Label htmlFor="hidden">Lock Album</Label>
<p className="text-muted-foreground text-sm">
Protect your album with a password
</p>
</div>
<Switch
id="locked"
<Label htmlFor="locked">Lock Album</Label>
<p className="text-muted-foreground text-sm">
Protect your album with a password
</p>
</div>
<Switch
id="locked"
🤖 Prompt for AI Agents
In `@frontend/src/components/Albums/CreateAlbumDialog.tsx` around lines 152 - 158,
The Label's htmlFor value ("hidden") doesn't match the Switch id ("locked") in
CreateAlbumDialog, so clicking the label won't toggle the switch; update the
Label htmlFor to "locked" (or change the Switch id to "hidden") so they match,
and ensure the underlying prop/state name (is_locked / is_hidden) used in
CreateAlbumDialog (and the similar case in EditAlbumDialog) is consistent with
the updated id to preserve accessibility and wiring to the form control.

Comment on lines 19 to 20
import { showInfoDialog } from '@/features/infoDialogSlice';
import { hideLoader, showLoader } from '@/features/loaderSlice';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove unused imports and variable — these cause the pipeline failure.

showInfoDialog, showLoader, hideLoader, and the dispatch variable are unused because useMutationFeedback handles loader/dialog dispatching internally. These warnings fail the ESLint check (max warnings: 0).

Proposed fix
-import { showInfoDialog } from '@/features/infoDialogSlice';
-import { hideLoader, showLoader } from '@/features/loaderSlice';
 }) => {
-  const dispatch = useDispatch();
   const [formData, setFormData] = useState({

Also remove the useDispatch import from line 2 if no other usage remains:

-import { useDispatch } from 'react-redux';

Also applies to: 28-29

🧰 Tools
🪛 GitHub Actions: PR Check

[warning] 19-19: eslint: 'showInfoDialog' is defined but never used. (no-unused-vars)


[warning] 20-20: eslint: 'hideLoader' is defined but never used. (no-unused-vars)


[warning] 20-20: eslint: 'showLoader' is defined but never used. (no-unused-vars)

🪛 GitHub Check: Linting

[warning] 20-20:
'showLoader' is defined but never used


[warning] 20-20:
'hideLoader' is defined but never used


[warning] 19-19:
'showInfoDialog' is defined but never used

🤖 Prompt for AI Agents
In `@frontend/src/components/Albums/EditAlbumDialog.tsx` around lines 19 - 20,
Remove unused imports and variable: delete showInfoDialog, showLoader,
hideLoader from the import list and remove the useDispatch import and the local
dispatch variable in EditAlbumDialog.tsx since useMutationFeedback now handles
loader/dialog dispatching; specifically remove references to the symbols
showInfoDialog, showLoader, hideLoader, useDispatch and the dispatch variable to
eliminate ESLint unused-import/variable warnings.

<Input
id="name"
placeholder="Enter album name"
value={formData.name}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mismatched htmlFor/id: Label says "hidden" but the Switch id is "locked".

The <Label htmlFor="hidden"> on line 152 won't associate with the <Switch id="locked"> on line 158. This is also inconsistent with the is_locked semantics.

Proposed fix
-              <Label htmlFor="hidden">Lock Album</Label>
+              <Label htmlFor="locked">Lock Album</Label>
🤖 Prompt for AI Agents
In `@frontend/src/components/Albums/EditAlbumDialog.tsx` at line 152, The Label's
htmlFor ("hidden") doesn't match the Switch's id ("locked") and doesn't reflect
the is_locked semantics; update them to match and be descriptive (e.g., change
Label htmlFor="hidden" to htmlFor="locked" or rename both to "is_locked") so the
<Label htmlFor="..."> pairs with <Switch id="..."> and the form field
(formData.is_locked / is_locked prop) consistently uses that identifier; ensure
any name/checked bindings for the Switch also use the same key (is_locked or
locked) throughout EditAlbumDialog.

Comment on lines 131 to 137
const confirmDelete = () => {
if (albumToDelete) {
deleteAlbumMutation.mutate(albumToDelete.id);
setIsDeleteDialogOpen(false);
setAlbumToDelete(null);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Stale closure: albumToDelete is null by the time the delete mutation succeeds.

confirmDelete sets albumToDelete to null synchronously (line 135), then the mutation runs asynchronously. When useMutationFeedback's onSuccess fires, the component has re-rendered with albumToDelete === null, so dispatch(removeAlbum(...)) never executes. The deleted album remains in the UI.

Proposed fix — capture the ID before mutating
  const confirmDelete = () => {
    if (albumToDelete) {
-     deleteAlbumMutation.mutate(albumToDelete.id);
-     setIsDeleteDialogOpen(false);
-     setAlbumToDelete(null);
+     const idToDelete = albumToDelete.id;
+     setIsDeleteDialogOpen(false);
+     setAlbumToDelete(null);
+     deleteAlbumMutation.mutate(idToDelete);
    }
  };

And update the feedback callback to not depend on the stale ref:

  useMutationFeedback(deleteAlbumMutation, {
    ...
    onSuccess: () => {
-     if (albumToDelete) {
-       dispatch(removeAlbum(albumToDelete.id));
-     }
+     refetch();
    },
  });

Alternatively, move the dialog close and state cleanup into onSuccess so the album reference is still available when the mutation completes.

Also applies to: 63-74

🤖 Prompt for AI Agents
In `@frontend/src/pages/Album/Album.tsx` around lines 131 - 137, confirmDelete
currently nulls out albumToDelete before the async mutation completes, causing
onSuccess handlers (from useMutationFeedback) to see albumToDelete === null and
fail to dispatch(removeAlbum(...)). Fix by capturing the id (e.g., const id =
albumToDelete?.id) before calling deleteAlbumMutation.mutate(id) and use that id
in the mutation feedback/onSuccess to dispatch(removeAlbum(id)) and only clear
setAlbumToDelete(null) and close the dialog after onSuccess (or perform those
cleanup actions inside onSuccess) so the deleted album reference remains
available to the feedback callback.

Comment on lines 139 to 154
const handleRefresh = async () => {
dispatch(showLoader('Refreshing albums...'));
try {
await refetch();
dispatch(hideLoader());
} catch (error) {
dispatch(hideLoader());
dispatch(
showInfoDialog({
title: 'Error',
message: 'Failed to refresh albums. Please try again.',
variant: 'error',
}),
);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

refetch() does not reject on query failure — the catch block is unreachable.

React Query's refetch() resolves with { data, error, isError, ... } regardless of success or failure. To handle errors, check the returned result instead.

Proposed fix
  const handleRefresh = async () => {
    dispatch(showLoader('Refreshing albums...'));
-   try {
-     await refetch();
+   const result = await refetch();
+   dispatch(hideLoader());
+   if (result.isError) {
+     dispatch(
+       showInfoDialog({
+         title: 'Error',
+         message: 'Failed to refresh albums. Please try again.',
+         variant: 'error',
+       }),
+     );
+   }
-     dispatch(hideLoader());
-   } catch (error) {
-     dispatch(hideLoader());
-     dispatch(
-       showInfoDialog({
-         title: 'Error',
-         message: 'Failed to refresh albums. Please try again.',
-         variant: 'error',
-       }),
-     );
-   }
  };
🤖 Prompt for AI Agents
In `@frontend/src/pages/Album/Album.tsx` around lines 139 - 154, handleRefresh
currently assumes refetch() throws on failure; change it to await the refetch()
result and check its properties (e.g., result.isError or result.error) instead
of relying on catch — always call dispatch(hideLoader()) after refetch returns,
and if result.isError or result.error is present call
dispatch(showInfoDialog(...)) with the error message/variant 'error'; update the
function name handleRefresh and the refetch call sites to use the returned
result object from refetch().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants